<template>
  <div>
    <template v-if="readonly" v-for="file in fileList">
      <el-text truncated>
        <el-icon>
          <Document/>
        </el-icon>
        {{ file }}
      </el-text>
    </template>
    <template v-else>
      <el-upload
        :limit="1"
        :on-exceed="handleExceed"
        ref="upload"
        action="#"
        :on-change="handleFileChange"
        :auto-upload="false"
        :show-file-list="false"
        drag
      >
        <div>
          <el-icon class="el-icon--upload">
            <upload-filled/>
          </el-icon>
          <div class="el-upload__text">
            <em>点击或拖拽上传</em>
          </div>
        </div>
        <template #tip>

          <el-row v-show="container.status === 'hash'">
            <el-col :span="6">
              hash进度：
            </el-col>
            <el-col :span="18">
              <el-progress style="margin-top: 5px" :text-inside="true" :stroke-width="17"
                           :percentage="hashPercentage"/>
            </el-col>
          </el-row>
          <el-row v-show="container.status === 'uploading' || container.status === 'uploaded' ">
            <el-col :span="6">
              上传进度：
            </el-col>
            <el-col :span="18">
              <el-progress style="margin-top: 5px" :text-inside="true" :stroke-width="17"
                           :percentage="fakeUploadPercentage"/>
            </el-col>
          </el-row>

          <div class="el-upload__tip text-red">限制一个文件, 新文件将会覆盖原文件</div>
          <div v-if="fileList.length > 0" class="custom-file-list">
            <el-row v-for="(file, index) in fileList"
                    :key="index"
            >
              <el-col :span="2">
                <el-icon>
                  <Document/>
                </el-icon>
              </el-col>
              <el-col :span="20">
                <el-text truncated>
                  {{ file }}
                </el-text>
              </el-col>
              <el-col :span="2">
                <el-button
                  type="primary"
                  text
                  style="width: 1em; height: 1em; margin-right: 8px"
                  icon="Delete"
                  @click="deleteFile(container.key)"
                />
              </el-col>
            </el-row>
          </div>
        </template>
      </el-upload>
    </template>

  </div>
</template>

<script setup lang="ts">
import {ref, computed, watch, nextTick} from 'vue'
import {ElMessage, genFileId} from 'element-plus'
import type {UploadInstance, UploadProps, UploadRawFile} from 'element-plus'
import {Upload} from "@/api/customer/files";
import {bigUploadRequest, deleteCustomerFileApi, mergeChunkFile, uploadFileApi} from "@/api/customer/files";
import {useUserStoreHook} from "@/store/modules/user";
import {UploadFilled} from '@element-plus/icons-vue'
import {usePublicCompStore} from "@/store/modules/publicComponents";
import {storeToRefs} from "pinia";
import {onBeforeRouteLeave} from "vue-router";

enum UploadStatusEnum {
  ready = 'ready',
  wait = 'wait',
  pause = 'pause',
  uploading = 'uploading',
  uploaded = 'uploaded',
  hash = 'hash'
}

const props = defineProps({
  readonly: {
    type: Boolean,
    default: false
  }
})

const PublicComStore = usePublicCompStore()
const {
  container,
  fileList,
  fakeUploadPercentage,
  hashPercentage,
  isUploadInterrupted
} = storeToRefs(PublicComStore)

// * 大文件分片上传,切片大小（20MB）
const SIZE: number = 20 * 1024 * 1024
// 最大可上传文件为800M
const maxSizeInBytes = 800 * 1024 * 1024
const upload = ref<UploadInstance>()
// 当前的请求xhr组成的数组
const requestListArr = ref<XMLHttpRequest[]>([])
// 组装的filechunk分段文件
let data = ref<Upload.data[]>([])
const status = ref<string>(UploadStatusEnum.wait)


const deleteFile = (fileKey: string) => {
  resetData()
  fileList.value = []
  deleteCustomerFileApi("undefined", fileKey)
    .then(() => {
    })
    .catch((error) => {
      ElMessage.error(error)
    })
}

const handleExceed: UploadProps['onExceed'] = (files) => {
  const file = files[0] as UploadRawFile
  if (file.size > maxSizeInBytes) {
    ElMessage.error("文件大小不能超过800M")
    return false
  }
  deleteFile(container.value.key)
  upload.value!.clearFiles()
  container.value.file = file
  file.uid = genFileId()
  upload.value!.handleStart(file)
}

// 计算：文件上传的进度
const uploadPercentage = computed({
  get() {
    if (!container.value.file || !data.value.length) return 0
    const loaded = data.value.map(item => item.size * item.percentage).reduce((acc, cur) => {
      return acc + cur
    })

    const percentage = parseInt((loaded / container.value.file.size!).toFixed(2))
    // 可能是开启了worker的缘故，导致计算结果可能会比100大，所以需要限制最大只能是99，在合并/秒传成功后再设置为100
    return Math.min(percentage, 98)
  },
  set(value) {
    return value
  }
})

// watch uploadPercentage，得到fakeUploadPercentage
watch(uploadPercentage, (newValue) => {
  if (newValue > fakeUploadPercentage.value) {
    fakeUploadPercentage.value = newValue
  }
})


// 选择了文件
const handleFileChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
  resetData()
  if (!uploadFile) return
  if (uploadFile.size > maxSizeInBytes) {
    ElMessage.error("文件大小不能超过800M")
    return false
  }
  container.value.file = uploadFile
  fileList.value.push(uploadFile.name)
  if (uploadFile.status === "ready") {
    isUploadInterrupted.value = false
    handlerUpload()
  }
}

// 重置
const resetData = () => {
  uploadPercentage.value = 0
  hashPercentage.value = 0
  container.value.status = ''
  fakeUploadPercentage.value = 0
  isUploadInterrupted.value = true
  requestListArr.value.forEach(xhr => xhr?.abort())
  requestListArr.value = []
  if (container.value.worker) {
    container.value.worker.onmessage = null
  }
}

// 上传
const handlerUpload = async () => {
  if (!container.value.file.raw) return
  // 点了上传按钮，状态改为上传中...
  status.value = UploadStatusEnum.uploading
  // 文件分片
  const fileChunkList = createFileChunk(container.value.file.raw)

  // 通过webworker计算出，文件hash
  container.value.hash = await calculateHash(fileChunkList)
  container.value.key = container.value.hash + "_" + container.value.file.name
  // uploadedList已上传的切片的切片文件名称
  const {shouldUpload, uploadedList, s3UploadID} = await verifyUpload(
    container.value.file.name,
    container.value.hash
  )

  container.value.s3UploadID = s3UploadID
  // 组装的filechunk数据先置空
  data.value = []

  // 服务器已经有完整文件了
  if (!shouldUpload) {
    fakeUploadPercentage.value = 100
    container.value.status = UploadStatusEnum.uploaded
    status.value = UploadStatusEnum.wait
    await nextTick()
    return ElMessage.success('秒传：上传成功')
  }

  data.value = fileChunkList.map(({file}, index) => ({
    fileHash: container.value.hash,
    index,
    chunkNumber: index + 1,    // S3分片上传时，chunkNumber需要从1开始
    chunk: file,
    size: file.size,
    // 如果已上传切片数组uploadedList中包含这个切片，则证明这个切片之前已经上传成功了，进度设为100。
    percentage: uploadedList.includes(index.toString()) ? 100 : 0,
  }))
  uploadChunks(uploadedList)
}


/**
 * @description: 生成文件切片
 * @param {*} file 上传的文件
 * @param {*} size  文件大小
 * @return {*}
 */
const createFileChunk = (file: UploadRawFile, size = SIZE) => {
  const fileChunkList = []
  let cur = 0
  while (cur < file.size) {
    fileChunkList.push({
      file: file.slice(cur, cur + size),
    })
    cur += size
  }
  return fileChunkList
}

/**
 * @description: 根据文件内容生成hash，而不是根据文件名称生成hash。
 * @description: 考虑到如果上传一个超大文件，读取文件内容计算 hash 是非常耗费时间的，并且会引起 UI 的阻塞，
 * @description: 导致页面假死状态，所以我们使用 web-worker 在 worker 线程计算 hash，这样用户仍可以在主界面正常的交互
 * @param {*} fileChunkList 切片数组
 * @return {*}
 */
const calculateHash = (fileChunkList: Upload.FileChunk[]) => {
  return new Promise<string>((resolve) => {
    // 更改当前状态
    container.value.status = UploadStatusEnum.hash
    // 开启worker
    container.value.worker = new Worker('/static/hash.js')
    // 向worker线程传入参数（注意传入的是对象，使用了解构写法）
    container.value.worker.postMessage({fileChunkList})
    // 接受来自worker线程的 加工后的回复
    container.value.worker.onmessage = (e: any) => {
      // console.log('calculateHash_Worker接收的参数', e);
      const {percentage, hash} = e.data
      // 若得到哈希值，则resolve返回
      hashPercentage.value = parseFloat(percentage.toFixed(2))
      if (hash) {
        resolve(hash)
      }
    }
  })
}

// https://blog.51cto.com/u_15091669/2608437  xhr对象POST请求、xhr兼容性、timeout、progress
/**
 * @description: 上传切片进度的回调函数,用闭包保存每个chunk的进度数据
 * @param {*} index 切片的索引
 * @param {*} item 每个切片
 * @return {*}
 */
const createProgressHandler = (index: number, item: Upload.data) => {
  return (e: ProgressEvent) => {
    if (e.lengthComputable) {
      item.percentage = parseInt(String((e.loaded / e.total) * 100))
    }
  }
}

/**
 * @description: 验证该文件是否需要上传，文件通过hash生成唯一，用于实现秒传
 * @param {*} filename 文件名
 * @param {*} fileHash 文件哈希值
 * @return {*}
 */
const verifyUpload = async (filename: string, fileHash: string) => {
  const {data} = await uploadFileApi({
    filename,
    fileHash
  })

  return {
    "shouldUpload": data.shouldUpload,
    "uploadedList": data.uploadedList,
    "s3UploadID": data.uploadID,
  }
}


/**
 * @description: 上传切片，同时过滤已上传的切片
 * @param {*} uploadedList 已经上传了的切片，这次不用上传了
 * @return {*}
 */
// 限制同时最多可发送三个上传块文件的请求（三个请求是一批次，一批次完成后才能进行下一批次，暂时不用）
const uploadChunks = async (uploadedList: string[] = []) => {
  container.value.status = UploadStatusEnum.uploading;
  const chunkData = data.value.filter(({chunkNumber}) => !uploadedList.includes(chunkNumber.toString()));

  const concurrentLimit = 3; // 最大并发请求数量
  const requestQueue = []; // 并发请求队列

  for (let i = 0; i < chunkData.length; i += concurrentLimit) {
    if (isUploadInterrupted.value) {
      break; // 如果上传被中断，停止继续上传
    }
    const chunkBatch = chunkData.slice(i, i + concurrentLimit);

    const batchRequests = chunkBatch.map(({chunk, chunkNumber, index}) => {
      const formData = new FormData();
      formData.append('chunk', chunk);
      formData.append('fileHash', container.value.hash);

      return {
        formData,
        chunkNumber,
        index,
      };
    });

    const batchPromises = batchRequests.map(async ({formData, chunkNumber, index}) => {
      if (isUploadInterrupted.value) {
        return  // 如果上传被中断，停止继续上传
      }
      const response = await bigUploadRequest({
        url: `${import.meta.env.VITE_BASE_API}/public/upload/file?num=${chunkNumber}&fileHash=${container.value.hash}&s3UploadID=${container.value.s3UploadID}&fileName=${container.value.key}`,
        data: formData,
        onProgress: createProgressHandler(index, data.value[index]),
        requestList: requestQueue,
        headers: {
          "x-token": useUserStoreHook().token,
        },
      });

      // 处理响应，根据实际情况处理
      return response;
    });

    // 将本次批量请求加入队列
    requestQueue.push(...batchPromises);

    // 等待本次批量请求完成
    await Promise.all(batchPromises);
  }

  // 所有切片上传完毕后，发个请求告诉后端：合并切片
  if (uploadedList.length + requestQueue.length === data.value.length) {
    mergeRequest();
  }
};


// 分片后直接发送所有请求，容易把客户端的带宽跑满
// const uploadChunks = async (uploadedList: string[] = []) => {
//   container.value.status = UploadStatusEnum.uploading
//   const requestList = data.value.filter(({hash}) => !uploadedList.includes(hash))
//     .map(({chunk, chunkNumber, index}) => {
//       const formData = new FormData()
//       // 切片文件
//       formData.append('chunk', chunk)
//       // 切片文件hash(暂时不需要这个)
//       // formData.append('hash', chunkNumber)
//       // 大文件hash
//       formData.append('fileHash', container.value.hash)
//       return {formData, chunkNumber, index}
//     })
//     .map(async ({formData, chunkNumber, index}) =>
//       bigUploadRequest({
//         url: `/api/public/upload/file?num=${chunkNumber}&fileHash=${container.value.hash}&s3UploadID=${container.value.s3UploadID}&fileName=${container.value.key}`,
//         data: formData,
//         onProgress: createProgressHandler(index, data.value[index]),
//         requestList: requestListArr.value,
//         headers: {
//           "x-token": useUserStoreHook().token
//         }
//       })
//     )
//
//   // 并发切片
//   await Promise.all(requestList)
//
//   // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
//   // 切片并发上传完以后，发个请求告诉后端：合并切片
//   if (uploadedList.length + requestList.length === data.value.length) {
//     mergeRequest()
//   }
// }

// 发请求通知服务器，合并切片
const mergeRequest = async () => {
  await mergeChunkFile({
    size: SIZE,
    fileHash: container.value.hash,
    filename: container.value.file.name,
    uploadID: container.value.s3UploadID
  })
  ElMessage.success('上传成功')
  fakeUploadPercentage.value = 100
  container.value.status = UploadStatusEnum.uploaded
  status.value = UploadStatusEnum.wait
}


// onBeforeRouteLeave 在路由切换离开页面时，终止上传
onBeforeRouteLeave((to, from, next) => {
  // 终止上传
  resetData()
  next(); // 继续页面切换
});

</script>

